{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "56db45b6",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| default_exp components"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "35d7e3af",
   "metadata": {},
   "source": [
    "# Components\n",
    "> `ft_html` and `ft_hx` functions to add some conveniences to `ft`, along with a full set of basic HTML components, and functions to work with forms and `FT` conversion"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8e2d405b",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "from dataclasses import dataclass, asdict, is_dataclass, make_dataclass, replace, astuple, MISSING\n",
    "from bs4 import BeautifulSoup, Comment\n",
    "from typing import Literal, Mapping, Optional\n",
    "\n",
    "from fastcore.utils import *\n",
    "from fastcore.xml import *\n",
    "from fastcore.meta import use_kwargs, delegates\n",
    "from fastcore.test import *\n",
    "from fasthtml.core import fh_cfg, unqid\n",
    "\n",
    "import types, json\n",
    "\n",
    "try: from IPython import display\n",
    "except ImportError: display=None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3ccb463a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections import UserDict\n",
    "from lxml import html as lx\n",
    "from pprint import pprint"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "17d5a90b",
   "metadata": {},
   "source": [
    "### Str and repr"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4b6e4407",
   "metadata": {},
   "source": [
    "In notebooks, FT components are rendered as their syntax highlighted XML/HTML:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f99aa358",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<p id=\"sentence_id\">\n",
       "<strong>FastHTML is <i>Fast</i></strong></p>\n",
       "\n",
       "```"
      ],
      "text/plain": [
       "p((strong(('FastHTML is ', i(('Fast',),{})),{}),),{'id': 'sentence_id'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sentence = P(Strong(\"FastHTML is \", I(\"Fast\")), id='sentence_id')\n",
    "sentence"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd6a5079",
   "metadata": {},
   "source": [
    "Elsewhere, they are represented as their underlying data structure:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "00e96e47",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "p((strong(('FastHTML is ', i(('Fast',),{})),{}),),{'id': 'sentence_id'})\n"
     ]
    }
   ],
   "source": [
    "print(repr(sentence))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dc101f0f",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@patch\n",
    "def __str__(self:FT): return self.id if self.id else to_xml(self, indent=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "601e0d75",
   "metadata": {},
   "source": [
    "If they have an id, then that id is used as the component's str representation:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9b3316f1",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'hx_target=#sentence_id'"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "f'hx_target=#{sentence}'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a10500cb",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@patch\n",
    "def __radd__(self:FT, b): return f'{b}{self}'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "977c8b24",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'hx_target=#sentence_id'"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "'hx_target=#' + sentence"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "992319c5",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@patch\n",
    "def __add__(self:FT, b): return f'{self}{b}'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2c3339cf",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'sentence_id...'"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sentence + '...'"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "810946a4",
   "metadata": {},
   "source": [
    "### fh_html and fh_hx"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0ff9acc3",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "named = set('a button form frame iframe img input map meta object param select textarea'.split())\n",
    "html_attrs = 'id cls title style accesskey contenteditable dir draggable enterkeyhint hidden inert inputmode lang popover spellcheck tabindex translate'.split()\n",
    "hx_attrs = 'get post put delete patch trigger target swap swap_oob include select select_oob indicator push_url confirm disable replace_url vals disabled_elt ext headers history history_elt indicator inherit params preserve prompt replace_url request sync validate'\n",
    "\n",
    "hx_evts = 'abort afterOnLoad afterProcessNode afterRequest afterSettle afterSwap beforeCleanupElement beforeOnLoad beforeProcessNode beforeRequest beforeSwap beforeSend beforeTransition configRequest confirm historyCacheError historyCacheMiss historyCacheMissError historyCacheMissLoad historyRestore beforeHistorySave load noSSESourceError onLoadError oobAfterSwap oobBeforeSwap oobErrorNoTarget prompt pushedIntoHistory replacedInHistory responseError sendAbort sendError sseError sseOpen swapError targetError timeout validation:validate validation:failed validation:halted xhr:abort xhr:loadend xhr:loadstart xhr:progress'\n",
    "js_evts = \"blur change contextmenu focus input invalid reset select submit keydown keypress keyup click dblclick mousedown mouseenter mouseleave mousemove mouseout mouseover mouseup wheel\"\n",
    "hx_attrs = [f'hx_{o}' for o in hx_attrs.split()]\n",
    "hx_attrs_annotations = {\n",
    "    \"hx_swap\": Literal[\"innerHTML\", \"outerHTML\", \"afterbegin\", \"beforebegin\", \"beforeend\", \"afterend\", \"delete\", \"none\"] | str,\n",
    "    \"hx_swap_oob\": Literal[\"true\", \"innerHTML\", \"outerHTML\", \"afterbegin\", \"beforebegin\", \"beforeend\", \"afterend\", \"delete\", \"none\"] | str,\n",
    "    \"hx_push_url\": Literal[\"true\", \"false\"] | str, \n",
    "    \"hx_replace_url\": Literal[\"true\", \"false\"] | str, \n",
    "    \"hx_disabled_elt\": Literal[\"this\", \"next\", \"previous\"] | str, \n",
    "    \"hx_history\": Literal[\"false\"] | str,\n",
    "    \"hx_params\": Literal[\"*\", \"none\"] | str,\n",
    "    \"hx_validate\": Literal[\"true\", \"false\"],\n",
    "}\n",
    "hx_attrs_annotations |= {o: str for o in set(hx_attrs) - set(hx_attrs_annotations.keys())}\n",
    "hx_attrs_annotations = {k: Optional[v] for k,v in hx_attrs_annotations.items()} \n",
    "hx_attrs = html_attrs + hx_attrs\n",
    "\n",
    "hx_evt_attrs = ['hx_on__'+camel2snake(o).replace(':','_') for o in hx_evts.split()]\n",
    "js_evt_attrs = ['hx_on_'+o for o in js_evts.split()]\n",
    "evt_attrs = js_evt_attrs+hx_evt_attrs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f904f825",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def attrmap_x(o):\n",
    "    if o.startswith('_at_'): o = '@'+o[4:]\n",
    "    return attrmap(o)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ee041be0",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "fh_cfg['attrmap']=attrmap_x\n",
    "fh_cfg['valmap' ]=valmap\n",
    "fh_cfg['ft_cls' ]=FT\n",
    "fh_cfg['auto_id']=False\n",
    "fh_cfg['auto_name']=True"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c8ade6b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def ft_html(tag: str, *c, id=None, cls=None, title=None, style=None, attrmap=None, valmap=None, ft_cls=None, **kwargs):\n",
    "    ds,c = partition(c, risinstance(Mapping))\n",
    "    for d in ds: kwargs = {**kwargs, **d}\n",
    "    if ft_cls is None: ft_cls = fh_cfg.ft_cls\n",
    "    if attrmap is None: attrmap=fh_cfg.attrmap\n",
    "    if valmap  is None: valmap =fh_cfg.valmap\n",
    "    if not id and fh_cfg.auto_id: id = True\n",
    "    if id and isinstance(id,bool): id = unqid()\n",
    "    kwargs['id'] = id.id if isinstance(id,FT) else id\n",
    "    kwargs['cls'],kwargs['title'],kwargs['style'] = cls,title,style\n",
    "    tag,c,kw = ft(tag, *c, attrmap=attrmap, valmap=valmap, **kwargs).list\n",
    "    if fh_cfg['auto_name'] and tag in named and id and 'name' not in kw: kw['name'] = kw['id']\n",
    "    return ft_cls(tag,c,kw, void_=tag in voids)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6434dea1",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<a @click.away=\"1\"></a>\n",
       "```"
      ],
      "text/plain": [
       "a((),{'@click.away': 1})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ft_html('a', **{'@click.away':1})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ed03b851",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<a @click.away=\"1\"></a>\n",
       "```"
      ],
      "text/plain": [
       "a((),{'@click.away': 1})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ft_html('a', {'@click.away':1})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "979e9bc8614fb3c1",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<a @click.away=\"1\"></a>\n",
       "```"
      ],
      "text/plain": [
       "a((),{'@click.away': 1})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ft_html('a', UserDict({'@click.away':1}))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1b62eed6",
   "metadata": {},
   "outputs": [],
   "source": [
    "c = Div(id='someid')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a979e0a6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<a id=\"someid\" name=\"someid\"></a>\n",
       "```"
      ],
      "text/plain": [
       "a((),{'id': 'someid', 'name': 'someid'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ft_html('a', id=c)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d5158b3d",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@use_kwargs(hx_attrs+evt_attrs, keep=True)\n",
    "def ft_hx(tag: str, *c, target_id=None, hx_vals=None, hx_target=None, **kwargs):\n",
    "    if hx_vals: kwargs['hx_vals'] = json.dumps(hx_vals) if isinstance (hx_vals,dict) else hx_vals\n",
    "    if hx_target: kwargs['hx_target'] = '#'+hx_target.id if isinstance(hx_target,FT) else hx_target\n",
    "    if target_id: kwargs['hx_target'] = '#'+target_id\n",
    "    return ft_html(tag, *c, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9f40e7f4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<a hx-vals='{\"a\": 1}'></a>\n",
       "```"
      ],
      "text/plain": [
       "a((),{'hx-vals': '{\"a\": 1}'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ft_hx('a', hx_vals={'a':1})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8ca91601",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<a hx-target=\"#someid\"></a>\n",
       "```"
      ],
      "text/plain": [
       "a((),{'hx-target': '#someid'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ft_hx('a', hx_target=c)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ede9b44d",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "_g = globals()\n",
    "_all_ = [\n",
    "    'A', 'Abbr', 'Address', 'Area', 'Article', 'Aside', 'Audio', 'B', 'Base', 'Bdi', 'Bdo', 'Blockquote', 'Body', 'Br',\n",
    "    'Button', 'Canvas', 'Caption', 'Cite', 'Code', 'Col', 'Colgroup', 'Data', 'Datalist', 'Dd', 'Del', 'Details', 'Dfn',\n",
    "    'Dialog', 'Div', 'Dl', 'Dt', 'Em', 'Embed', 'Fencedframe', 'Fieldset', 'Figcaption', 'Figure', 'Footer', 'Form',\n",
    "    'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'Head', 'Header',\n",
    "    'Hgroup', 'Hr', 'I', 'Iframe', 'Img', 'Input', 'Ins', 'Kbd', 'Label', 'Legend', 'Li',\n",
    "    'Link', 'Main', 'Map', 'Mark', 'Menu', 'Meta', 'Meter', 'Nav', 'Noscript', 'Object', 'Ol', 'Optgroup', 'Option', 'Output',\n",
    "    'P', 'Picture', 'PortalExperimental', 'Pre', 'Progress', 'Q', 'Rp', 'Rt', 'Ruby', 'S', 'Samp', 'Script', 'Search',\n",
    "    'Section', 'Select', 'Slot', 'Small', 'Source', 'Span', 'Strong', 'Style', 'Sub', 'Summary', 'Sup', 'Table', 'Tbody',\n",
    "    'Td', 'Template', 'Textarea', 'Tfoot', 'Th', 'Thead', 'Time', 'Title', 'Tr', 'Track', 'U', 'Ul', 'Var', 'Video', 'Wbr']\n",
    "for o in _all_: _g[o] = partial(ft_hx, o.lower())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1b030be9",
   "metadata": {},
   "source": [
    "For tags that have a `name` attribute, it will be set to the value of `id` if not provided explicitly:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ecbcfa18",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<form hx-post=\"/\" hx-target=\"#tgt\" id=\"frm\" name=\"frm\"><button hx-target=\"#foo\" id=\"btn\" name=\"btn\"></button></form>\n",
       "```"
      ],
      "text/plain": [
       "form((button((),{'hx-target': '#foo', 'id': 'btn', 'name': 'btn'}),),{'hx-post': '/', 'hx-target': '#tgt', 'id': 'frm', 'name': 'frm'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Form(Button(target_id='foo', id='btn'),\n",
    "     hx_post='/', target_id='tgt', id='frm')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fab04fb3",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def File(fname):\n",
    "    \"Use the unescaped text in file `fname` directly\"\n",
    "    return NotStr(Path(fname).read_text())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8843495e",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<input name=\"nm\">\n",
       "\n",
       "```"
      ],
      "text/plain": [
       "input((),{'name': 'nm'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a = Input(name='nm')\n",
    "a"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a8bbd57c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<input name=\"nm\" hx-swap-oob=\"true\">\n",
       "\n",
       "```"
      ],
      "text/plain": [
       "input((),{'name': 'nm', 'hx-swap-oob': 'true'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a(hx_swap_oob='true')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "67146285",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<input name=\"nm\" hx-swap-oob=\"true\">\n",
       "\n",
       "```"
      ],
      "text/plain": [
       "input((),{'name': 'nm', 'hx-swap-oob': 'true'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "a"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1579f7f1",
   "metadata": {},
   "source": [
    "### show"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7861dfe6",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def show(ft, *rest, iframe=False, height='auto', style=None):\n",
    "    \"Renders FT Components into HTML within a Jupyter notebook.\"\n",
    "    if isinstance(ft, str): ft = Safe(ft)\n",
    "    if rest: ft = (ft,)+rest\n",
    "    res = to_xml(ft)\n",
    "    if iframe:\n",
    "        style = \"border: none; \" + (style or \"\")\n",
    "        cfg = dict(frameborder=0, width='100%', height=height, style=style)\n",
    "        res = to_xml(Iframe(srcdoc=res, **cfg))\n",
    "    with warnings.catch_warnings():\n",
    "        warnings.simplefilter(\"ignore\", UserWarning)\n",
    "        display.display(display.HTML(res))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a723b84b",
   "metadata": {},
   "source": [
    "When placed within the `show()` function, this will render the HTML in Jupyter notebooks."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8d0c2583",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<p id=\"sentence_id\">\n",
       "<strong>FastHTML is <i>Fast</i></strong></p>\n"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "show(sentence)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a01d3046",
   "metadata": {},
   "source": [
    "You can also display full embedded pages in an iframe:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "30d3422d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<iframe srcdoc='&lt;!doctype html&gt;\n",
       "&lt;html&gt;\n",
       "  &lt;head&gt;\n",
       "    &lt;link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css\"&gt;\n",
       "  &lt;/head&gt;\n",
       "  &lt;body&gt;\n",
       "    &lt;h2&gt;Heading 2&lt;/h2&gt;\n",
       "    &lt;p&gt;Paragraph&lt;/p&gt;\n",
       "  &lt;/body&gt;\n",
       "&lt;/html&gt;\n",
       "' width=\"100%\" height=\"100\" style=\"border: none; \"></iframe>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "picocss = \"https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css\"\n",
    "picolink = (Link(rel=\"stylesheet\", href=picocss))\n",
    "\n",
    "fullpage = Html(\n",
    "    Head(picolink),\n",
    "    Body(\n",
    "        H2(\"Heading 2\"),\n",
    "        P(\"Paragraph\")\n",
    "    )\n",
    ")\n",
    "\n",
    "show(fullpage, height=100, iframe=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7db522d2",
   "metadata": {},
   "source": [
    "Plain strings also work:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "727d32b3",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<p>Hi <i>everyone</i></p>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "show(\"<p>Hi <i>everyone</i></p>\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "41a00fe7",
   "metadata": {},
   "source": [
    "### fill_form and find_inputs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1df362c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def _fill_item(item, obj):\n",
    "    if not isinstance(item,FT): return item\n",
    "    tag,cs,attr = item.list\n",
    "    if isinstance(cs,tuple): cs = tuple(_fill_item(o, obj) for o in cs)\n",
    "    name = attr.get('name', None)\n",
    "    val = None if name is None else obj.get(name, None)\n",
    "    if val is not None and not 'skip' in attr:\n",
    "        if tag=='input':\n",
    "            if attr.get('type', '') == 'checkbox':\n",
    "                if isinstance(val, list):\n",
    "                    if attr['value'] in val: attr['checked'] = '1'\n",
    "                    else: attr.pop('checked', '')\n",
    "                elif val: attr['checked'] = '1'\n",
    "                else: attr.pop('checked', '')\n",
    "            elif attr.get('type', '') == 'radio':\n",
    "                if val and val == attr['value']: attr['checked'] = '1'\n",
    "                else: attr.pop('checked', '')\n",
    "            else: attr['value'] = val\n",
    "        if tag=='textarea': cs=(val,)\n",
    "        if tag == 'select':\n",
    "            if isinstance(val, list):\n",
    "                for opt in cs:\n",
    "                    if opt.tag == 'option' and opt.get('value') in val:\n",
    "                        opt.selected = '1'\n",
    "            else:\n",
    "                option = next((o for o in cs if o.tag=='option' and o.get('value')==val), None)\n",
    "                if option: option.selected = '1'\n",
    "    return FT(tag,cs,attr,void_=item.void_)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f0c83f26",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def fill_form(form:FT, obj)->FT:\n",
    "    \"Fills named items in `form` using attributes in `obj`\"\n",
    "    if is_dataclass(obj): obj = asdict(obj)\n",
    "    elif not isinstance(obj,dict): obj = obj.__dict__\n",
    "    return _fill_item(form, obj)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "caef04d9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<form><fieldset name=\"stuff\">    <input value=\"Profit\" id=\"title\" class=\"char\" name=\"title\">\n",
       "<label class=\"px-2\">      <input type=\"checkbox\" name=\"done\" data-foo=\"bar\" class=\"checkboxer\" checked=\"1\">\n",
       "Done</label>    <input type=\"hidden\" id=\"id\" name=\"id\" value=\"2\">\n",
       "<select name=\"opt\"><option value=\"a\"></option><option value=\"b\" selected=\"1\"></option></select><textarea id=\"details\" name=\"details\">Details</textarea><button>Save</button></fieldset></form>\n",
       "```"
      ],
      "text/plain": [
       "form((fieldset((input((),{'value': 'Profit', 'id': 'title', 'class': 'char', 'name': 'title'}), label((input((),{'type': 'checkbox', 'name': 'done', 'data-foo': 'bar', 'class': 'checkboxer', 'checked': '1'}), 'Done'),{'class': 'px-2'}), input((),{'type': 'hidden', 'id': 'id', 'name': 'id', 'value': 2}), select((option((),{'value': 'a'}), option((),{'value': 'b', 'selected': '1'})),{'name': 'opt'}), textarea(('Details',),{'id': 'details', 'name': 'details'}), button(('Save',),{})),{'name': 'stuff'}),),{})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "@dataclass\n",
    "class TodoItem:\n",
    "    title:str; id:int; done:bool; details:str; opt:str='a'\n",
    "\n",
    "todo = TodoItem(id=2, title=\"Profit\", done=True, details=\"Details\", opt='b')\n",
    "check = Label(Input(type=\"checkbox\", cls=\"checkboxer\", name=\"done\", data_foo=\"bar\"), \"Done\", cls='px-2')\n",
    "form = Form(Fieldset(Input(cls=\"char\", id=\"title\", value=\"a\"), check, Input(type=\"hidden\", id=\"id\"),\n",
    "                     Select(Option(value='a'), Option(value='b'), name='opt'),\n",
    "                     Textarea(id='details'), Button(\"Save\"),\n",
    "                     name=\"stuff\"))\n",
    "form = fill_form(form, todo)\n",
    "assert '<textarea id=\"details\" name=\"details\">Details</textarea>' in to_xml(form)\n",
    "form"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1513cf64",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<form><select multiple=\"1\" name=\"items\"><option value=\"a\" selected=\"1\">a</option><option value=\"b\">b</option><option value=\"c\" selected=\"1\">c</option></select></form>\n",
       "```"
      ],
      "text/plain": [
       "form((select((option(('a',),{'value': 'a', 'selected': '1'}), option(('b',),{'value': 'b'}), option(('c',),{'value': 'c', 'selected': '1'})),{'multiple': '1', 'name': 'items'}),),{})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "@dataclass\n",
    "class MultiSelect:\n",
    "    items: list[str]\n",
    "\n",
    "multiselect = MultiSelect(items=['a', 'c'])\n",
    "multiform = Form(Select(Option('a', value='a'), Option('b', value='b'), Option('c', value='c'), multiple='1', name='items'))\n",
    "multiform = fill_form(multiform, multiselect)\n",
    "assert '<option value=\"a\" selected=\"1\">a</option>' in to_xml(multiform)\n",
    "assert '<option value=\"b\">b</option>' in to_xml(multiform)\n",
    "assert '<option value=\"c\" selected=\"1\">c</option>' in to_xml(multiform)\n",
    "multiform"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dd316f94-0695-44bf-96c2-45128a8b0644",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<form><fieldset><label>      <input type=\"checkbox\" name=\"items\" value=\"a\" checked=\"1\">\n",
       "a</label><label>      <input type=\"checkbox\" name=\"items\" value=\"b\">\n",
       "b</label><label>      <input type=\"checkbox\" name=\"items\" value=\"c\" checked=\"1\">\n",
       "c</label></fieldset></form>\n",
       "```"
      ],
      "text/plain": [
       "form((fieldset((label((input((),{'type': 'checkbox', 'name': 'items', 'value': 'a', 'checked': '1'}), 'a'),{}), label((input((),{'type': 'checkbox', 'name': 'items', 'value': 'b'}), 'b'),{}), label((input((),{'type': 'checkbox', 'name': 'items', 'value': 'c', 'checked': '1'}), 'c'),{})),{}),),{})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "@dataclass\n",
    "class MultiCheck:\n",
    "    items: list[str]\n",
    "\n",
    "multicheck = MultiCheck(items=['a', 'c'])\n",
    "multiform = Form(Fieldset(Label(Input(type='checkbox', name='items', value='a'), 'a'),\n",
    "                          Label(Input(type='checkbox', name='items', value='b'), 'b'),\n",
    "                          Label(Input(type='checkbox', name='items', value='c'), 'c')))\n",
    "multiform = fill_form(multiform, multicheck)\n",
    "assert '<input type=\"checkbox\" name=\"items\" value=\"a\" checked=\"1\">' in to_xml(multiform)\n",
    "assert '<input type=\"checkbox\" name=\"items\" value=\"b\">' in to_xml(multiform)\n",
    "assert '<input type=\"checkbox\" name=\"items\" value=\"c\" checked=\"1\">' in to_xml(multiform)\n",
    "multiform"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8b171490",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def fill_dataclass(src, dest):\n",
    "    \"Modifies dataclass in-place and returns it\"\n",
    "    for nm,val in asdict(src).items(): setattr(dest, nm, val)\n",
    "    return dest"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "77e3f785",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "TodoItem(title='Profit', id=2, done=True, details='Details', opt='b')"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "nt = TodoItem('', 0, False, '')\n",
    "fill_dataclass(todo, nt)\n",
    "nt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c9594f70",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def find_inputs(e, tags='input', **kw):\n",
    "    \"Recursively find all elements in `e` with `tags` and attrs matching `kw`\"\n",
    "    if not isinstance(e, (list,tuple,FT)): return []\n",
    "    inputs = []\n",
    "    if isinstance(tags,str): tags = [tags]\n",
    "    elif tags is None: tags = []\n",
    "    cs = e\n",
    "    if isinstance(e, FT):\n",
    "        tag,cs,attr = e.list\n",
    "        if tag in tags and kw.items()<=attr.items(): inputs.append(e)\n",
    "    for o in cs: inputs += find_inputs(o, tags, **kw)\n",
    "    return inputs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7e740aac",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[input((),{'value': 'Profit', 'id': 'title', 'class': 'char', 'name': 'title'})]"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "inps = find_inputs(form, id='title')\n",
    "test_eq(len(inps), 1)\n",
    "inps"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04b4f6c7",
   "metadata": {},
   "source": [
    "You can also use lxml for more sophisticated searching:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e551de91",
   "metadata": {},
   "outputs": [],
   "source": [
    "elem = lx.fromstring(to_xml(form))\n",
    "test_eq(elem.xpath(\"//input[@id='title']/@value\"), ['Profit'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1d8a28b1",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def __getattr__(tag):\n",
    "    if tag.startswith('_') or tag[0].islower(): raise AttributeError\n",
    "    tag = tag.replace(\"_\", \"-\")\n",
    "    def _f(*c, target_id=None, **kwargs): return ft_hx(tag, *c, target_id=target_id, **kwargs)\n",
    "    return _f"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bff4f07b",
   "metadata": {},
   "source": [
    "### html2ft"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "afb0f65c",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "_re_h2x_attr_key = re.compile(r'^[A-Za-z_-][\\w-]*$')\n",
    "def html2ft(html, attr1st=False):\n",
    "    \"\"\"Convert HTML to an `ft` expression\"\"\"\n",
    "    rev_map = {'class': 'cls', 'for': 'fr'}\n",
    "\n",
    "    def _parse(elm, lvl=0, indent=4):\n",
    "        if isinstance(elm, str): return repr(elm.strip(\"\\n\")) if elm.strip() else ''\n",
    "        if isinstance(elm, list): return '\\n'.join(_parse(o, lvl) for o in elm)\n",
    "        tag_name = elm.name.capitalize().replace(\"-\", \"_\")\n",
    "        if tag_name=='[document]': return _parse(list(elm.children), lvl)\n",
    "        cts = elm.contents\n",
    "        cs = [repr(c.strip(\"\\n\")) if isinstance(c, str) else _parse(c, lvl+1)\n",
    "              for c in cts if str(c).strip()]\n",
    "        attrs, exotic_attrs  = [], {}\n",
    "        for key, value in sorted(elm.attrs.items(), key=lambda x: x[0]=='class'):\n",
    "            if value is None or value == True: value = True  # handle boolean attributes\n",
    "            elif isinstance(value,(tuple,list)): value = \" \".join(value)\n",
    "            key, value = rev_map.get(key, key), value or True\n",
    "            if _re_h2x_attr_key.match(key): attrs.append(f'{key.replace(\"-\", \"_\")}={value!r}')\n",
    "            else: exotic_attrs[key] = value\n",
    "        if exotic_attrs: attrs.append(f'**{exotic_attrs!r}')\n",
    "        spc = \" \"*lvl*indent\n",
    "        onlychild = not cts or (len(cts)==1 and isinstance(cts[0],str))\n",
    "        j = ', ' if onlychild else f',\\n{spc}'\n",
    "        inner = j.join(filter(None, cs+attrs))\n",
    "        if onlychild:\n",
    "            if not attr1st: return f'{tag_name}({inner})'\n",
    "            else:\n",
    "                # respect attr1st setting\n",
    "                attrs = ', '.join(filter(None, attrs))\n",
    "                return f'{tag_name}({attrs})({cs[0] if cs else \"\"})'\n",
    "        if not attr1st or not attrs: return f'{tag_name}(\\n{spc}{inner}\\n{\" \"*(lvl-1)*indent})' \n",
    "        inner_cs = j.join(filter(None, cs))\n",
    "        inner_attrs = ', '.join(filter(None, attrs))\n",
    "        return f'{tag_name}({inner_attrs})(\\n{spc}{inner_cs}\\n{\" \"*(lvl-1)*indent})'\n",
    "\n",
    "    soup = BeautifulSoup(html.strip(), 'html.parser')\n",
    "    for c in soup.find_all(string=risinstance(Comment)): c.extract()\n",
    "    return _parse(soup, 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3569a964",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```python\n",
       "Form(\n",
       "    Fieldset(\n",
       "        Input(value='Profit', id='title', name='title', cls='char'),\n",
       "        Label(\n",
       "            Input(type='checkbox', name='done', data_foo='bar', checked='1', cls='checkboxer'),\n",
       "            'Done',\n",
       "            cls='px-2'\n",
       "        ),\n",
       "        Input(type='hidden', id='id', name='id', value='2'),\n",
       "        Select(\n",
       "            Option(value='a'),\n",
       "            Option(value='b', selected='1'),\n",
       "            name='opt'\n",
       "        ),\n",
       "        Textarea('Details', id='details', name='details'),\n",
       "        Button('Save'),\n",
       "        name='stuff'\n",
       "    )\n",
       ")\n",
       "```"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "h = to_xml(form)\n",
    "hl_md(html2ft(h), 'python')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "af92d7d6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```python\n",
       "Form(\n",
       "    Fieldset(name='stuff')(\n",
       "        Input(value='Profit', id='title', name='title', cls='char')(),\n",
       "        Label(cls='px-2')(\n",
       "            Input(type='checkbox', name='done', data_foo='bar', checked='1', cls='checkboxer')(),\n",
       "            'Done'\n",
       "        ),\n",
       "        Input(type='hidden', id='id', name='id', value='2')(),\n",
       "        Select(name='opt')(\n",
       "            Option(value='a')(),\n",
       "            Option(value='b', selected='1')()\n",
       "        ),\n",
       "        Textarea(id='details', name='details')('Details'),\n",
       "        Button()('Save')\n",
       "    )\n",
       ")\n",
       "```"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hl_md(html2ft(h, attr1st=True), 'python')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c6203402",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def sse_message(elm, event='message'):\n",
    "    \"Convert element `elm` into a format suitable for SSE streaming\"\n",
    "    data = '\\n'.join(f'data: {o}' for o in to_xml(elm).splitlines())\n",
    "    return f'event: {event}\\n{data}\\n\\n'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "50d18540",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "event: message\n",
      "data: <div>\n",
      "data:   <p>hi</p>\n",
      "data:   <p>there</p>\n",
      "data: </div>\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "print(sse_message(Div(P('hi'), P('there'))))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "defc22f0",
   "metadata": {},
   "source": [
    "## Tests"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "46083529",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "def test_html2ft(html: str, attr1st=False):\n",
    "    # html -> ft -> html\n",
    "    soup1 = BeautifulSoup(html, 'html.parser')\n",
    "    soup2 = BeautifulSoup(to_xml(eval(html2ft(html, attr1st))).strip(), 'html.parser')\n",
    "    assert soup1.prettify() == soup2.prettify()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "517bcd86",
   "metadata": {},
   "outputs": [],
   "source": [
    "test_html2ft('<input value=\"Profit\" name=\"title\" id=\"title\" class=\"char\">', attr1st=True)\n",
    "test_html2ft('<input value=\"Profit\" name=\"title\" id=\"title\" class=\"char\">')\n",
    "test_html2ft('<div id=\"foo\"></div>')\n",
    "test_html2ft('<div id=\"foo\">hi</div>')\n",
    "test_html2ft('<div>Howdy <a href=\"https://answer.ai\">answer</a> how are you?</div>')\n",
    "test_html2ft('<div x-show=\"open\" x-transition:enter=\"transition duration-300\" x-transition:enter-start=\"opacity-0 scale-90\">Hello 👋</div>')\n",
    "test_html2ft('<div x-transition:enter.scale.80 x-transition:leave.scale.90>hello</div>')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "99773c69",
   "metadata": {},
   "outputs": [],
   "source": [
    "assert html2ft('<div id=\"foo\">hi</div>', attr1st=True) == \"Div(id='foo')('hi')\"\n",
    "assert html2ft('<div id=\"foo\" hidden>hi</div>', attr1st=True) == \"Div(id='foo', hidden=True)('hi')\"\n",
    "assert html2ft(\"\"\"\n",
    "  <div x-show=\"open\" x-transition:enter=\"transition duration-300\" x-transition:enter-start=\"opacity-0 scale-90\">Hello 👋</div>\n",
    "\"\"\") == \"Div('Hello 👋', x_show='open', **{'x-transition:enter': 'transition duration-300', 'x-transition:enter-start': 'opacity-0 scale-90'})\"\n",
    "assert html2ft('<div x-transition:enter.scale.80 x-transition:leave.scale.90>hello</div>') == \"Div('hello', **{'x-transition:enter.scale.80': True, 'x-transition:leave.scale.90': True})\"\n",
    "assert html2ft(\"<img alt=' ' />\") == \"Img(alt=' ')\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "474e14b4",
   "metadata": {},
   "source": [
    "## Export -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d211e8e2",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "import nbdev; nbdev.nbdev_export()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "74aeda124534ac06",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 5
}
